package net.gdface.service.search;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.Future;
import java.util.concurrent.locks.ReentrantLock;

import net.gdface.httpclient.AsyncClientManager;
import net.gdface.httpclient.HostManager;
import net.gdface.httpclient.HttpClientUtility;
import net.gdface.httpclient.SyncClientManager;
import net.gdface.image.LazyImage;
import net.gdface.image.NotImageException;
import net.gdface.image.UnsupportedFormatException;
import net.gdface.service.client.Status;
import net.gdface.utils.Assert;
import net.gdface.utils.SimpleTypes;
import net.gdface.utils.Judge;
import net.gdface.worker.NoTimeupException;
import net.gdface.worker.RetryException;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 图像文件下载对象<br>
 * 使用{@link HttpClientUtility}的同步和异步请求方法,实现对指定 {@link #uri}位置的图像文件下载
 * 
 * @see AsyncClientManager#getResponseAsync(URI, URI, org.apache.http.client.config.RequestConfig, FutureCallback, org.apache.http.NameValuePair...)
 * @see SyncClientManager#getResponse(URI, org.apache.http.client.config.RequestConfig, org.apache.http.NameValuePair...)
 * @author guyadong
 *
 */
public class RemoteImage {
	static final Logger logger = LoggerFactory.getLogger(RemoteImage.class);
	/**
	 * ImageIO.read同步锁
	 */
	private static final ReentrantLock LOCK_IMAGEIO_READ = new ReentrantLock();
	/**
	 * 内存低限阀值(MB)
	 */
	private static long memoryThreshold = 128;
	private static int imgMinWidth = 128;
	private static int imgMinHeight = 128;
	private static int imgMaxWidth = 2048;
	private static int imgMaxHeight = 2048;

	// //////////////////////////////////////////////
	/**
	 * 图像地址
	 */
	private final URI uri;
	/**
	 * 图像引用页面
	 */
	private final URI referer;
	/**
	 * 是否为重试，出错是否<font color="red" size="3"><b>不</b></font>抛出异常<br>
	 * 为false时,当下载出现异常或图像数据不合要求时会抛出{@link RetryException}
	 */
	private boolean retryed;

	/**
	 * 是否有效图片,<br>
	 * true 图像格式可识别，尺寸符合要求<br>
	 * false 不是图片(不能生成{@link BufferedImage}对象)或尺寸不合要求或图像格式不能识别
	 */
	private boolean isValidImage = false;
	/**
	 * 处理图像的工作状态
	 */
	private Status status = Status.NULL;
	/**
	 * class name for exception
	 */
	private String exception = null;
	/**
	 * 图像数据对象
	 */
	private LazyImage image=null;
	/**主机访问间隔时间未到时的动作类型<br> 
	 *  {@link #THROW}抛出异常<br>
	 *  {@link #IGNORE}忽略<br>
	 *  {@link #SLEEP}休眠
	 */
	public enum ActionIfNoTimeup {	THROW,IGNORE,	SLEEP}
	private final static String _getCauseName(Throwable e) {
		return (SimpleTypes.getCause(e)).getClass().getCanonicalName();
	}

	/**
	 * 检查图像尺寸是否符合要求
	 * 
	 * @param width
	 * @param height
	 * @param rm
	 * @return {@link Status#NULL} 符合要求的图片<br>
	 *         {@link Status#TOO_LARGE} 图片尺寸太大<br>
	 *         {@link Status#TOO_SMALL} 图片尺寸太小
	 */
	private Status checkSize(int width, int height) {
		if (width < imgMinWidth || height < imgMinHeight)
			return Status.TOO_SMALL;
		else if (width * height > imgMaxWidth * imgMaxHeight)// 长宽之乘积小于最大尺寸的长宽乘积
			return Status.TOO_LARGE;
		return Status.NULL;
	}
	
	/**
	 * 检查主机访问间隔时间,如果时间未到，则由{@link ActionIfNoTimeup}决定后续动作
	 * @param actionIfNoTimeup
	 * @throws NoTimeupException
	 * @throws InterruptedException
	 */
	private void checkTimeup(final ActionIfNoTimeup actionIfNoTimeup) throws NoTimeupException, InterruptedException {
		// retrty状态下，会先打开引用页面，不会被服务器拒绝
		if (retryed || actionIfNoTimeup == ActionIfNoTimeup.IGNORE)
			HostManager.getInstance().setHit(uri);
		else {
			switch (actionIfNoTimeup) {
			case SLEEP:
				HostManager.getInstance().sleepUnlessTimeup(uri, -1, 0, 0);
			case THROW:
				long gap;
				if ((gap = HostManager.getInstance().setHitIfTimeup(uri, -1, 0, 0)) < 0)
					// 主机访问间隔未到延期执行,Exception Message为所差的时间(毫秒)
					throw new NoTimeupException(-gap);
			default:
			}
		}
	}

	/**
	 * 同步方式下载指定位置{@link #uri}的图像数据<br>
	 * 当{@link #retryed}为{@code false}时,下载出错会抛出抛出异常时
	 * 
	 * @param actionIfNoTimeup
	 *            主机访问间隔时间未到时的动作<br>
	 *            参见 {@link #checkTimeup(ActionIfNoTimeup)}
	 * @param openReferer
	 *            是否先打开引用图片的网页{@link #referer}
	 * 
	 * @return
	 * @throws RetryException
	 * @throws InterruptedException
	 * @throws NoTimeupException
	 */
	public RemoteImage downloadSync(ActionIfNoTimeup actionIfNoTimeup, final boolean openReferer) throws RetryException, NoTimeupException, InterruptedException {
		CloseableHttpResponse response = null;
		checkTimeup (actionIfNoTimeup);
		try {
			if (openReferer && null != referer && referer.isAbsolute()
					&& 0 <= HostManager.getInstance().setHitIfTimeup(referer, HostManager.DEFAULT_PAGE_INTERVAL_MILLS, 0, 0)) {
				// 先尝试打开引用图片的网页
				SyncClientManager.getInstance().getPage(referer, null);
			}
			response = SyncClientManager.getInstance().getResponse(uri, null, new BasicNameValuePair("Referer", referer.toString()));
			onCompleted(response);
		} catch (IOException e) {
			// 只处理IOException，放过RetryException
			onFailed(e);
		} finally {
			try {
				if (null != response)
					response.close();
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}
		return this;
	}

	/**
	 * 异步下载指定位置{@link #uri}的图像数据<br>
	 * 
	 * @param actionIfNoTimeup
	 *            主机访问间隔时间未到时的动作<br>
	 *            参见 {@link #checkTimeup(ActionIfNoTimeup)}
	 * @param openReferer
	 *            是否打开引用页面{@link #referer}
	 * @param callback
	 *            回调函数对象<br>
	 *            参见
	 *            {@link org.apache.http.impl.nio.client.CloseableHttpAsyncClient#execute(org.apache.http.client.methods.HttpUriRequest, FutureCallback)}
	 * 
	 * @return
	 * @throws NoTimeupException
	 *             主机访问间隔时间未到
	 * @throws InterruptedException
	 * @see AsyncClientManager#getResponseAsync(URI, URI, org.apache.http.client.config.RequestConfig, FutureCallback,
	 *      org.apache.http.NameValuePair...)
	 */
	public Future<HttpResponse> downloadAsyn(final ActionIfNoTimeup actionIfNoTimeup, final boolean openReferer, final FutureCallback<HttpResponse> callback) throws NoTimeupException, InterruptedException {
		Assert.notNull(callback, "callback");		
		checkTimeup (actionIfNoTimeup);
		// 确定是否要先打开图片所在网页
		URI ref = (openReferer && null != referer && referer.isAbsolute() && 0 <= HostManager.getInstance().setHitIfTimeup(referer,
				HostManager.DEFAULT_PAGE_INTERVAL_MILLS, 0, 0)) ? referer : null;
		return AsyncClientManager.getInstance().getResponseAsync(uri, ref, null, callback,
				new BasicNameValuePair("Referer", referer.toString()));
	}

	/**
	 * 同步方式下载指定位置{@link #uri}的图像数据
	 * @param actionIfNoTimeup 
	 * @param imgUri
	 *            图像地址
	 * @param referer
	 *            图像引用网页地址
	 * @param openReferer
	 *            是否先打开引用图片的网页{@link #referer}
	 * @param throwFail
	 *            出错时是否抛出异常
	 * 
	 * @return {@link RemoteImage}实例
	 * @throws RetryException
	 * @throws InterruptedException 
	 * @throws NoTimeupException 
	 * @see #downloadSync(ActionIfNoTimeup, boolean)
	 */
	public static RemoteImage downloadSync(ActionIfNoTimeup actionIfNoTimeup, URI imgUri, URI referer, final boolean openReferer, boolean throwFail)
			throws RetryException, NoTimeupException, InterruptedException {
		RemoteImage rmi = new RemoteImage(imgUri, referer, !throwFail);
		return rmi.downloadSync(actionIfNoTimeup, openReferer);
	}

	/**
	 * 当http请求抛出异常时，根据异常类型更新 {@link #status}状态并记录异常类型{@link #exception}
	 * 
	 * @param ex
	 * @return
	 */
	public RemoteImage onFailed(Exception ex) {
		try {
			throw ex;
		} catch (ClientProtocolException e) {
			status = Status.FAIL_DOWNLOAD;
			exception = _getCauseName(e);
		} catch (java.net.SocketTimeoutException e) {
			status = Status.TIMEOUT;
			exception = _getCauseName(e);
		} catch (org.apache.http.conn.ConnectionPoolTimeoutException e) {
			status = Status.TIMEOUT;
			exception = _getCauseName(e);
			logger.warn(e.toString());
		} catch (org.apache.http.conn.ConnectTimeoutException e) {
			status = Status.TIMEOUT;
			exception = _getCauseName(e);
		} catch (java.net.ConnectException e) {
			status = Status.FAIL_DOWNLOAD;
			exception = _getCauseName(e);
		} catch (org.apache.http.NoHttpResponseException e) {
			status = Status.FAIL_DOWNLOAD;
			exception = _getCauseName(e);
		} catch (java.net.UnknownHostException e) {
			status = Status.FAIL_DOWNLOAD;
			exception = _getCauseName(e);
		} catch (IOException e) {
			status = Status.FAIL_DOWNLOAD;
			exception = _getCauseName(e);
		} catch (Exception e) {
			status = Status.FAIL_DOWNLOAD;
			exception = _getCauseName(e);
		} finally {
		}
		return this;
	}

	/**
	 * 当http请求被正常响应时，下载图像数据，并检查数据有效性 更新 {@link #status}状态,并记录异常类型{@link #exception}<br>
	 * 当{@link #retryed}为{@code true}时,下载出错会抛出抛出异常时
	 * 
	 * @param result
	 * @return
	 * @throws RetryException
	 */
	public RemoteImage onCompleted(HttpResponse result) throws RetryException {
		final HttpEntity entity = result.getEntity();
		try {
			final byte[] imgBytes = EntityUtils.toByteArray(entity);
			if (!Judge.isEmpty(imgBytes)) {
				this.image = LazyImage.create(imgBytes);
				if (Status.NULL == (this.status = checkSize(image.getWidth(), image.getHeight()))) {
					this.isValidImage = true;
					this.exception=null;
				}
			} else {
				this.status = Status.ZERODATA;
				// 如果返回内容长度为空则再尝试下载一次
				if (!this.retryed) {
					throw new RetryException();
				}
			}
		} catch (IOException e) {
			this.status = Status.FAIL_DOWNLOAD;
			this.exception = _getCauseName(e);
			if (!this.retryed) {
				throw new RetryException(e);
			}
		} catch (NotImageException e) {
			this.status = Status.NOTIMAGE;
			// 如果返回内容长度为空则再尝试下载一次
			if (!this.retryed)
				throw new RetryException();
		} catch (UnsupportedFormatException e) {
			this.status = Status.UNSUPIMAGE;
		}
		return this;
	}

	/**
	 * 异步下载指定位置{@link #uri}的图像数据
	 * @param throwIfNotOntime 参见{@link #downloadAsyn(ActionIfNoTimeup, boolean, FutureCallback)}
	 * @param uri
	 *            图像地址
	 * @param referer
	 *            图像引用网页地址
	 * @param openReferer
	 *            是否打开引用页面{@link #referer}
	 * @param callback
	 *            回调函数对象<br>
	 *            参见
	 *            {@link org.apache.http.impl.nio.client.CloseableHttpAsyncClient#execute(org.apache.http.client.methods.HttpUriRequest, FutureCallback)}
	 * 
	 * @return
	 * @throws NoTimeupException 
	 * @throws InterruptedException 
	 * @see #downloadAsyn(ActionIfNoTimeup, boolean, FutureCallback)
	 */
	public static Future<HttpResponse> downloadAsync(ActionIfNoTimeup throwIfNotOntime, URI uri, URI referer,
			boolean openReferer, FutureCallback<HttpResponse> callback) throws NoTimeupException, InterruptedException {
		Assert.notNull(uri, "uri");
		Assert.notNull(callback, "callback");
		RemoteImage rmi = new RemoteImage(uri, referer, openReferer);
		return rmi.downloadAsyn(throwIfNotOntime, openReferer, callback);
	}

	/**
	 * 设置图像尺寸过滤参数
	 * 
	 * @param minWidth
	 * @param minHeight
	 * @param maxWidth
	 * @param maxHeight
	 */
	public static void setImageSizeLimit(int minWidth, int minHeight, int maxWidth, int maxHeight) {
		imgMinWidth = minWidth;
		imgMinHeight = minHeight;
		imgMaxWidth = maxWidth;
		imgMaxHeight = maxHeight;
	}

	/**
	 * @param uri
	 *            图像数据位置
	 * @param ref
	 *            图像引用页面位置
	 * @param retryed
	 *            参见{@link #retryed}
	 * @throws IllegalArgumentException
	 *             {@code uri}为{@code null}
	 */
	public RemoteImage(URI uri, URI ref, boolean retryed) throws IllegalArgumentException {
		Assert.notNull(uri, "uri");
		this.uri = uri;
		this.referer = ref;
		this.retryed = retryed;
	}

	/**
	 * 通过本地文件创建对象<br>
	 * {@link #retryed}设置为{@code true}<br>
	 * 为减少不必要的文件IO操作<br>
	 * {@link #isValidImage}设置为{@code true},不检查图像数据有效性，自动默认图像为有效图像 <br>
	 * 不检查{@code md5}与文件实际数据的一致性
	 * 
	 * @param uri
	 * @param ref
	 * @param localFile
	 * @param md5
	 *            文件md5校验码
	 * @throws FileNotFoundException
	 *             {@code localFile}不存在或不是文件或0长度
	 * @throws UnsupportedFormatException 
	 * @throws NotImageException 
	 * @see #LazyImage(URI, URI, boolean)
	 */
	public RemoteImage(URI uri, URI ref, File localFile, String md5) throws FileNotFoundException,
			UnsupportedFormatException, NotImageException {
		this(uri, ref, true);
		Assert.notNull(localFile, "localFile");
		Assert.notEmpty(md5, "md5");
		if (!localFile.exists() || !localFile.isFile() || 0 == localFile.length())
			throw new FileNotFoundException(String.format("NOT EXIST OR NOT FILE OR ZERO bytes%s",
					localFile.getAbsolutePath()));
		this.image = LazyImage.create(localFile, md5);
		if (Status.NULL == (this.status = checkSize(image.getWidth(), image.getHeight()))) {
			this.isValidImage = true;
		}
	}

	/**
	 * @return the exception
	 */
	public String getException() {
		return exception;
	}

	/**
	 * @return the referer
	 */
	public URI getReferer() {
		return referer;
	}

	/**
	 * @return the status
	 */
	public Status getStatus() {
		return status;
	}

	/**
	 * @return the uri
	 */
	public URI getUri() {
		return uri;
	}

	/**
	 * @see #retryed
	 * @return retryed
	 */
	public boolean isRetryed() {
		return retryed;
	}

	/**
	 * @see #isValidImage
	 * @return the isValidImage
	 */
	public boolean isValidImage() {
		return isValidImage;
	}

	/**
	 * @param retryed
	 *            要设置的 retryed
	 */
	public void setRetryed(boolean retryed) {
		this.retryed = retryed;
	}

	/**
	 * @param status
	 *            要设置的 status
	 */
	public void setStatus(Status status) {
		this.status = status;
	}

	/**
	 * @return image
	 */
	public LazyImage getImage() {
		return image;
	}
	public static void main(String args[]) throws FileNotFoundException, IOException {

	}

}
